Effective C++ 2

  • Created on 2014-05

条款19:设计class犹如设计type
treat class design as type design.

注意:
1.新type的对戏那个如何被创建和销毁:
以及构造、析构函数,内存分配和释放函数。
2.对象的初始化和对象的赋值有什么区别。
3.新type对象如果被以值传递,意味着什么?
4.什么是新type的合法值?
5.你的新type需要配合某个继承图系吗?(inheritance graph)
6.你的新type需要什么样的类型转换?
7.什么样的操作符和函数对此新type是合理的?
8.什么样的标准函数应该驳回?(注意必须声明为private者)
9.谁该取用新type的成员?决定哪些是private/public/protected/friend 等。
10.什么是新type的未声明接口(undeclared interface)?
11.你的新type有多么一般化?考虑它成为一个类模版。class template


条款20:宁以pass-by-reference-to-const替换pass-by-value
perfer pass-by-reference-to-const to pass-by-value.

缺省情况下C++以by value方式传递对象至函数,
函数参数就是实际实参的复本,由对象的copy构造函数提供。

pass-by-reference-to-const效率更高,减少了复本对象及其成员对象的构造和析构。
而且还可以避免slicing(对象切割)的问题。
当一个derived class对象以by-value方式传递并被视为base class对象,
base class的copy构造函数会被调用,导致derived class对象的那些特化兴致被切割掉了,
只剩下一个base class对象。

一般而言,可以合理假设:内置对象和STL的迭代器和函数对象,可以pass-by-value!


条款21:必须返回对象时,别妄想返回其reference
don't try to return a reference when you must return an object.

任何函数如果返回一个reference或pointer指向某个local对象,都会一败涂地!

TestObj& retLocalObj2(){
                 TestObj a(2);
                 return a;
}

TestObj& retLocalObj3(){
                 TestObj *a = new TestObj(3);
                 return *a;
}

TestObj* retLocalObj4(){
                 TestObj *a = new TestObj(4);
                 return a;
}

以上第一个例中,local object建立在栈上,返回时local object已会被释放。
第二、三个例中,local object建立在堆上,返回的local object不会被释放,
但是,之后谁能对这些临时的对象释放,delete?例如:
w = retLocalObj2() retLocalObj3() * retLocalObj4(); 
明显没有机会释放中间变量,导致内存泄漏。

正确做法:
必须返回新对象,就让那个函数直接返回一个新对象
inline const Rational operator*(const Rational& lhs, const Rational& rhs){
     return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
必须承受由此带来的构造、析构成本。

绝不要返回pointer和reference指向一个local stack对象,
或返回reference指向一个heap-allocated对象,
或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。


条款22:将成员变量声明为private
declare data members private

切记将成员变量声明为private。这可赋予客户访问数据的一致性、
可细微划分放哪高温控制、允诺约束条件获得保证,
并提供class作者以充分的实现弹性和日后的修改空间。
protected并不比public更具有封装性,
private可以使其成员变量,对其derived class更有封装性。


条款23:宁以non-member、non-friend替换member函数
prefer non-member non-friend functions to member function

class WebBrowser{
public:
     void clearCache();
     void clearHistory();
     void removeCookie();
     // void clearEverything(); // 使用这个成员函数调用前三个函数不够好
     ...
}
void clearBrowser(){
     wb.clearCache();
     wb...
} // 这样更好,有更好的封装性、包裹弹性(packaging flexible)和机能扩充性。
为什么呢?
因为,在类内,越少的代码能够做同一件事,封装性越好
clearEverything()也做到了clearCache()...等的函数的工作。

在所有函数必须定义在类内的语言来说,
可以另外定义一个WebBrowser的工具类utility class,
在其中定义一个static member函数完成相关功能。


条款24:若所有参数皆需要类型转换,请为此采用non-member函数
declare non-member functions when type conversions should apply to all parameters.

class Rational{
public:
     ...
     const Rational operator*(const Rational* rhs) const;
}
result = oneHalf * 2; // 正确
result = 2 * oneHalf; // 错误!int 2 无法隐式转换为Rational类型

将operator*在类外定义即可:
const Rational operator*(const Rational* lhs, const Rational* rhs){...}
之前出错的语句也可以正常运行了!

其实可以将它定义为class Rational的friend函数,但是应该尽量避免,原因未详述。

如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)
进行类型转换,那么这个函数必须是个non-member。


条款25:考虑写出一个不抛出异常的swap函数
consider support for a non-throwing swap.

1.当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常;
2.如果你提供一个member swap,也该提供一个non-member swap用来调用前者。
对于classes(而非templates),也请特化std::swap。
3.调用swap时应针对std::swap使用using声明式,然后调用swap,
并且不带有任何“命名空间资格修饰”。
4.为“用户定义类型”进行std templates全特化是最好的,
但千万不要尝试在std内加入某些对std而言全新的东西。

namespace std{
     template<typename T>
 // std::swap的典型实现;
     void swap(T& a, T& b){
          T temp(a); // 只要T类型支持copying构造函数和copy assignment操作符即可
          a = b;
          b = temp;
     }
}

但是有些用户定义类型,复制的动作并非总有必要。因为,
主要情况是,有些成员变量只是指针,指向一个对象,内含真正数据,
(这种设计的常见表现形式是所谓的pimpl手法——pointer to implementation)
两个对象只需要交换这个指针值即可。
所以,可以针对这个用户定义类型,让std::swap进行特化:
namespace std{
     template<>     // 这是std::swap针对T是Widget的特化版本
     void swap<Widget>(Widget& a, Widget& b){
          swap(a.pImpl, b.pImpl);
     }
} // 但无法通过编译,因为pImpl是private成员。

真正解决方法:
class Widget{
public:
     void swap(Widget& other){
          using std::swap; // 令std::swap在此函数内可用
          swap(pImpl, other.pImpl); // 编译器根据实际情况,
               // 调用T专属的版本,或者std中一般化(泛化)的版本
     }
}
namespace std{
     template<>
     void swap<Widget>(Widget& a, Widget& b){
          a.swap(b);
     }
}

劝告:成员版swap绝不可抛出异常。

条款26:尽可能延后变量定义式的出现时间
postpone variable definitions as long as possible.

1.当一个变量需要使用时,才去声明它。
2.为了提高效率,构造时就初始化好它!例:
std::string encryted; // 先使用default构造函数
encrypted = password; // 再用赋值操作符……
不如
std::string encryted(password); // 使用copy构造函数初始化了

循环时怎么办?
Widget w;
for(...){
     w = xxx;
}
还是
for(...){
     Widget w = xxx;
}
前者效率高一点,但是w作用域扩大,可理解性和易维护性变差!

只有两种情况才使用前者的做法:
(1)知道赋值比“构造+析构”的成本低
(2)你正在处理代码中效率高度敏感的部分(performance-sensitive)

条款27:尽量少做转型操作
minimize casting

(T)expr; // 两种旧式转型
T(expr);

const_cast<T>(expr); // 将对象的常量性去除(cast away the constness)

dynamic_cast<T>(expr); // 安全向下转型(safe downcasting)
          // 决定某对象是否归属继承体系中的某个类型(之后细谈)
          // 唯一无法由旧式语法执行的动作

static_cast<T>(expr); // 强迫隐式转换(implicit conversions)
          // 将non-const转换为const,反向操作不能,只能用const_cast
          // 或将int转成double,或相反
          // 将void*转成type*
          // 将ptr-to-base转为ptr-to-derived

reinterpret_cast<T>(expr); // 企图进行低级转型,实际动作和结果取决于编译器
          // 所以它不可移植。将一个long或int转成指针都可以。

新式转型比旧式:
(1)更加容易辨认,易读
(2)转型动作的目标窄化,编译器容易判断出错误

1.尽量避免转型,在注重效率的代码中,避免dynamic_cast,
最好试着发展无须转型的替代设计。
2.如转型是必要的,试着将它隐藏于某个函数背后。
客户可以调用该函数,使其不用将转型过程置于其代码中。
3.宁可使用新式转型语法,不要使用旧式转型。清晰。


条款28:避免返回handles指向对象内部成分
avoid returning "handles" to object internals.

避免返回handles(包括references、ptr、iterator迭代器)指向对象内部,
保证封装性,帮助const成员函数的行为像个const,
并将“虚吊号码牌”(dangling handles)的可能性降至最低。
虚吊号码牌,即是野指针,对象已被销毁,但是指向这个地方的指针还在


条款29:为“异常安全”而努力是值得的
strive for exception-safe code.

exception-safe 异常安全 的两个条件:
当异常抛出时,
(1)不泄露任何资源。
不会代码的出错中断,导致没有delete或者释放掉资源、互斥锁等。
(2)不允许数据败坏。
因为new失败,可能导致一个指针成为野指针。
内部的变量、状态,非原子性,不一致。

异常安全函数——提供以下三个保证之一:
(1)基本承诺。
若异常被抛出,程序内的任何事物仍然保持在有效状态下。
(2)强烈保证。
若一场抛出,程序状态不改变。(即是变化都是原子性的。)
要不是成功执行的状态,要不处于函数调用前的状态。
(3)不抛掷(throw)保证。
绝不抛出异常。总是能够完成承诺的功能。

条款29给了我很大震动,这个条款很长,还是从原书重读较好。
因为没有想到,这个代码的严谨性超过了我以前的想象!
以下给出最好的那个代码版本:
struct PMImpl{
     std::tr1::shared_ptr<Image> bgImage;
     int imageChanges;
}; // 为了swap-and-copy而设计的(之前的条款有说)
class PrettyMenu{
     ...
private:
     Mutex mutex; // 互斥量
     std::tr1::shared_ptr<PMImpl> pImpl; // 为了swap-and-copy而设计的
};
void PrettyMenu::changeBackground(std::istream& imgSrc){
     using std::swap;
     Lock ml(&mutex);
     std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
     pNew->bgImge.reset(new Image(imgSrc));
     ++pNew->imageChanges;
     swap(pImpl, pNew);
}

“强烈保证”往往能够以copy-and-swap实现出来,
但强烈保证并非对所有函数都可实现或具有现实意义。


条款30:透彻了解inlining的里里外外
understand the ins and outs of inlining

在class声明处,就定义函数过程的,都会隐喻为inline。

virtual函数不能够被inline。
千万别将构造和析构函数inline!

调试器,无法对inline函数设置断点。

将大多数inlining限制在小型、被频繁调用的函数身上。
这可使日后的调试过程和二进制升级(binaryupgradability)更容易,
也可使潜在的代码膨胀问题最小化,使程序速度提升的机会最大化。

不要只因为function template出现在头文件,就将它们声明为inline。
0%